import  * as fs from "fs";
import request from "request";
import cheerio from "cheerio";

/**
 * @param { import("domhandler").Node } node
 * @return { node is import("domhandler").Element }
 */
const isElement = function(node) {
    return node.type !== undefined;
};

/**
 * @param { import("domhandler").Element | import("domhandler").Node } node
 * @return { node is import("domhandler").DataNode }
 */
const isDataNode = function(node) {
    return node.type === "text";
};

/**
 * @param { import("domhandler").Node } node
 */
const getELementText = function(node) {
    let text = "";
    if (isElement(node)) {
        if (isDataNode(node)) {
            const data = node.data.replace(/(.)[ \n\t]+|[ \n\t]+(.)/g, (full, left, right) => `${left || ''} ${right || ''}`);
            text += data;
        } else {
            for (const child of node.children) {
                text += getELementText(child);
            }
        }
    }
    return text;
};

/**
 * @param { string } type
 * @param { string[] } save
 */
const parseUseType = function(type, save) {
    if (!type) {
        return;
    }
    if (type.includes("<")) {
        type.split(/\<|\>/g).forEach(item => parseUseType(item, save));
    } else if(type.includes(",")) {
        type.split(/,/g).forEach(item => parseUseType(item, save));
    } else {
        type = type.trim();
        if (type.endsWith("[]")) {
            type = type.slice(0, -2);
        }
        save.push(type);
    }
};

/**
 * @param { string } path
 * @param { string } text
 */
const saveFile = function(path, text) {
    const folder = path.slice(0, path.lastIndexOf("\\"));
    fs.mkdirSync(folder, { recursive: true });
    fs.writeFileSync(path, text);
}

/**
 * @param { import("domhandler").Node } ul
 * @param { { name: string, link?: string }[] } [list]
 */
const foreachNode = function(ul, list = []) {
    if (!isElement(ul) || ul.name !== "ul") {
        return;
    }
    ul.children.forEach(li => {
        if (!isElement(li) || li.name !== "li") {
            return;
        }
        const nodes = li.childNodes;
        const ulIndex = nodes.findIndex(node => isElement(node) && node.name === "ul");
        const nameNodes = ulIndex !== -1 && ulIndex < 2 ? nodes.slice(0, ulIndex) : nodes.slice(0, 2);
        const name = nameNodes.map(node => getELementText(node)).join("");
        const twoElement = nodes[1];
        const link = isElement(twoElement) && twoElement.name === "a" ? twoElement.attribs.href : undefined;
        list.push({ name: name, link });
        if (ulIndex !== -1) {
            foreachNode(nodes[ulIndex], list);
        }
    });
    return list;
};

/**
 * @param { string } url
 * @return { Promise<string[]> }
 */
const getLinkList = function(url) {
    return new Promise((resolve, reject) => {
        request(url, function (error, response, body) {
            const baseUrl = url.replace(/\/[^/]+$/, "");
            const $ = cheerio.load(body);
            const uls = $(".contentContainer > ul").toArray();
            const list = [];
            $(".indexContainer > ul")[0].children
                .forEach(li => {
                    if(isElement(li) && li.name === "li") {
                        const firstNode = li.children[0];
                        if (isElement(firstNode) && firstNode.name === "a") {
                            const link = firstNode.attribs.href;
                            list.push(`${baseUrl}/${link}`);
                        }
                    }
                });
            resolve(list);
        });
    });
}

/**
 * @param {string } url
 * @return { Promise<{ classPath: string; packages: string[]; className: string; text: string; useTypes: string[] }> }
 */
const stringifyFile = async function(url) {
    return new Promise((resolve, reject) => {
        request(url, function (error, response, body) {
            const $ = cheerio.load(body);
            const subTitle = getELementText($(".subTitle")[0]);
            const regExp = /(abstract|static)? (class|interface|enum) ([a-zA-Z._]+)( extends ([a-zA-Z._]+))?( implements ([a-zA-Z._]+))?/;
            const preElement = $("li.blockList > pre")[0];
            const preResult = getELementText(preElement).match(regExp);
            const [,classType, dataType, classNameFull,, classExtendsName,, classImplName] = preResult;
            const classPath = `${subTitle}.${classNameFull}`;
            const classPaths = classPath.split(".");
            const className = classPaths[classPaths.length - 1];
            const packages = classPaths.slice(0, -1);
            const useTypes = [];
            parseUseType(classExtendsName, useTypes);
            parseUseType(classImplName, useTypes);
            const text = `export${
                classType === "abstract" ?
                    ` abstract`
                :
                    ""
            } ${
                "class /** interface */ "/* dataType */ // 因为interface不能生命静态属性
            } ${
                className
            }${
                classExtendsName ?
                    ` extends ${classExtendsName}`
                :
                    ""
            }${
                classImplName ?
                    ` implements ${classImplName}`
                :
                    ""
            } {\n${
                $(".memberSummary").toArray().map((memberSummary) =>
                    $("tr", memberSummary).toArray().slice(1).map((element) => {
                        const colOne = $(".colOne", element)[0];
                        if (colOne) {
                            const text = getELementText(colOne).split("\n")[0];
                            if (!text.includes("(")) {
                                return `\t${text};`
                            } else {
                                return `\t${
                                    text
                                        .replace(/^[^(]+\(/, "constructor(")
                                        .replace(/([a-zA-Z._\[\]<>]+)\u00a0([a-zA-Z._\[\]<>]+)/g,
                                            (full, type, variable) => {
                                                parseUseType(type, useTypes);
                                                return `${variable}: ${type}`;
                                            }
                                        )
                                };`
                            }
                        } else {
                            const funcType = getELementText($(".colFirst", element)[0]);
                            const funcTypeResult = funcType.match(/^((?:(?:public|private|protected) )?(?:(?:static|abstract) )?)?(.*?)$/);
                            const funcStr = getELementText($(".colLast", element)[0]).split("\n")[0];
                            const leftParenthesisIndex = funcStr.indexOf("(");
                            const funcName = funcStr.slice(0, leftParenthesisIndex);
                            const funcParams = funcStr.slice(leftParenthesisIndex);
                            parseUseType(funcTypeResult[2], useTypes);
                            if (funcType.endsWith("class ") || funcType.endsWith("interface ")) {
                                return "";
                            }
                            return `\t${
                                funcTypeResult[1] || ""
                            }${
                                funcName === className ?
                                    "constructor"
                                :
                                    funcName
                            }${
                                funcParams.replace(/([a-zA-Z._\[\]<>]+)\u00a0([a-zA-Z._\[\]<>]+)/g,
                                    (full, type, variable) => {
                                        parseUseType(type, useTypes);
                                        return `${variable}: ${type}`;
                                    }
                                )
                            }${
                                funcTypeResult[2] ?
                                    `: ${funcTypeResult[2]}`
                                :
                                    ""
                            };`;
                        }
                    }).join("\n")
                ).join("\n\n")
            }\n}`;
            resolve({ classPath, packages, className, text, useTypes });
        });
    });
};

const TARGET_PATH = process.cwd() + "\\target";
const PACKAGES_NODE = "Packages";

/**
 * @param { string[] } linkList
 */
const saveTreeListToFile = async function(linkList) {
    /** @type { { classPath: string; packages: string[]; className: string; text: string; useTypes: string[]; }[] } */
    const list = [];
    for (const link of linkList) {
        list.push(await stringifyFile(link));
    }
    for (const info of list) {
        const { useTypes, packages } = info;
        const importOriginTypeSet = new Set();
        const importTypes = [];
        for (const type of Array.from(new Set(useTypes))) {
            if (type.startsWith("java.")) {
                continue;
            }
            if (["boolean", "byte", "short", "int", "long", "float", "double"].includes(type)) {
                continue;
            }
            const originTypes = type.split(".");
            const originType = originTypes[0];
            if (importOriginTypeSet.has(originType)) {
                continue;
            }
            const findType = list.find(item => item.classPath.endsWith(type));
            if (findType) {
                const findPackages = findType.packages.slice(0, findType.packages.length + 1 - originTypes.length);
                const equalsIndex = new Array(Math.max(packages.length, findPackages.length) + 1)
                    .fill(null)
                    .findIndex((_, index) => (!packages[index] && !findPackages[index]) || packages[index] !== findPackages[index]);
                const backLength = packages.length - equalsIndex;
                const backPath = backLength === 0 ? ["."] : new Array(backLength).fill("..");
                const forwardPath = findPackages.slice(equalsIndex);
                const relativePath = [...backPath, ...forwardPath, originType].join("/");
                importTypes.push(`import { ${originType} } from "${relativePath}";`);
                importOriginTypeSet.add(originType);
            }
        }
        if (importTypes.length > 0) {
            info.text = importTypes.join("\n") + "\n\n" + info.text;
        }
    }
    for (const { packages, className, text } of list) {
        const path = TARGET_PATH + "\\" + PACKAGES_NODE + "\\" + packages.join("\\") + "\\" + className + ".d.ts";
        saveFile(path, text);
    }
};

/**
 * @param { string } root
 * @param { string } folder
 * @param { "root" | "node" } [posoition]
 */
const makeDtsFile = function(root, folder, posoition) {
    const path = `${root}\\${folder}`;
    const dirs = fs.readdirSync(path, { withFileTypes: true });
    const save = [];
    dirs.forEach(info => {
        const name = info.name;
        if (info.isDirectory()) {
            makeDtsFile(path, name, posoition === undefined ? "root" : "node");
            save.push(name);
        }
        else if (info.isFile() && name.endsWith(".d.ts") && name !== "index.d.ts" && name !== "global.d.ts") {
            save.push(name.slice(0, -5));
        }
    });
    if (posoition !== undefined) {
        const dtsfileStr = `export namespace ${folder} {\n${
            save.map(name =>
                `\timport("./${name}"); export * from "./${name}";`
            ).join("\n")
        }\n}`;
        saveFile(`${path}\\index.d.ts`, dtsfileStr);
    }
    if (posoition === "root") {
        const dtsfileStr = `declare namespace ${folder} {\n${
            save.map(name =>
                `\timport("./${name}"); export * from "./${name}";`
            ).join("\n")
        }\n}`;
        saveFile(`${path}\\global.d.ts`, dtsfileStr);
    }
};

async function main () {
    const list = [
        await getLinkList("http://192.168.2.182:7001/documentation/javadocs/triggers/allclasses-frame.html"),
        await getLinkList("http://192.168.2.182:7001/documentation/javadocs/api/allclasses-frame.html"),
        await getLinkList("http://192.168.2.182:7001/documentation/javadocs/tmapi/allclasses-frame.html"),
    ].flat();
    await saveTreeListToFile(list);
    makeDtsFile(TARGET_PATH, PACKAGES_NODE, "root");
    // makeDtsFile(process.cwd() + "\\src", "types");
}

main();
